Skip to content

registry: per-identity token rotation via token_min_iat (fixes #2205) - #2208

Open
hognek wants to merge 4 commits into
jaylfc:devfrom
hognek:tsk-nslyhc-registry-token-iat
Open

registry: per-identity token rotation via token_min_iat (fixes #2205)#2208
hognek wants to merge 4 commits into
jaylfc:devfrom
hognek:tsk-nslyhc-registry-token-iat

Conversation

@hognek

@hognek hognek commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Closes #2205.

What

Add token_min_iat INTEGER column (default 0) to agent_registry so registry JWT tokens can be invalidated per-identity without revoking the whole identity.

Changes

  • Migration v6: idempotent ALTER TABLE with default 0 — existing rows keep their tokens valid so the migration cannot lock the fleet out
  • Store: AgentRegistryStore.bump_token_min_iat(canonical_id, ts) — persists the cutoff
  • Auth path: after the status check in _verify_agent_scope, reject tokens whose iat claim is strictly less than the record's token_min_iat with 401 "token superseded"
  • Route: POST /api/agents/registry/{id}/rotate-tokens — session-owner/admin route that bumps the cutoff and writes a forensic audit-log entry (action: rotate-tokens)
  • No exp claims — noted as follow-up in the issue; this is the smallest mechanism

Tests (9 new, all passing)

Store-level: bump sets cutoff, default zero on registration, nonexistent returns None
Auth-path: old token rejected after bump (401), new token passes after bump, default-zero keeps existing tokens valid
Route: admin rotates, owner rotates, nonexistent returns 404

Rotation flow

bump token_min_iat → re-mint token → distribute new token

No existing test regressions — 138 store + 43 route + 11 auth = all green.

Summary by CodeRabbit

  • New Features

    • Added token rotation for registered identities, allowing owners or administrators to invalidate previously issued tokens.
    • Added safeguards that reject superseded tokens while continuing to accept newer tokens.
    • Token rotation returns the updated identity record and reports not-found identities appropriately.
    • Added governance audit details for token rotation events.
  • Tests

    • Added coverage for token cutoff persistence, authentication behavior, authorization, successful rotation, and missing identities.

@gitar-bot

gitar-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds per-identity token_min_iat persistence, rejects superseded registry tokens, and adds an owner-or-admin token rotation endpoint with governance auditing. Store and integration tests cover cutoff behavior, authentication, successful rotation, and missing identities.

Changes

Token rotation

Layer / File(s) Summary
Token cutoff persistence
tinyagentos/agent_registry_store.py
Adds an idempotent token_min_iat migration and a monotonic bump_token_min_iat store method.
Superseded-token authentication
tinyagentos/agent_token_auth.py
Rejects tokens with an iat earlier than the registry record cutoff.
Token rotation route and audit
tinyagentos/routes/agent_registry.py
Adds the rotation endpoint, owner-or-admin authorization, cutoff updates, audit fields, and missing-record handling.
Rotation and authentication validation
tests/test_agent_registry_store.py, tests/test_token_rotation.py
Tests default and persisted cutoffs, token acceptance and rejection, authorized rotation, and 404 responses.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant rotate_tokens
  participant AgentRegistryStore
  participant GovernanceAudit
  Caller->>rotate_tokens: POST /api/agents/registry/{canonical_id}/rotate-tokens
  rotate_tokens->>AgentRegistryStore: load record and authorize owner or admin
  rotate_tokens->>AgentRegistryStore: bump_token_min_iat(canonical_id, current timestamp)
  rotate_tokens->>GovernanceAudit: record before and after cutoff values
  rotate_tokens-->>Caller: updated registry record
Loading

Possibly related PRs

  • jaylfc/taOS#2235: Modifies the same registry token authentication and rotation areas with JWT expiration and renewal.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies per-identity token rotation through token_min_iat and references the related issue.
Linked Issues check ✅ Passed The changes implement the requirements in [#2205], including migration, cutoff enforcement, rotation authorization, audit logging, and tests.
Out of Scope Changes check ✅ Passed The implementation and tests remain within the token invalidation and rotation scope defined by [#2205].
Docstring Coverage ✅ Passed Docstring coverage is 80.65% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Registry: per-identity token rotation via token_min_iat

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add per-identity JWT invalidation using agent_registry.token_min_iat cutoff
• Expose owner/admin rotate-tokens endpoint and enforce cutoff in registry auth
• Extend collab delegation D1 with sponsor tracking, invite metadata, and peer dispatch
Diagram

graph TD
A["Caller (admin/owner)"] --> B["POST rotate-tokens"] --> C["AgentRegistryStore"] --> D[("agent_registry table")]
E["Agent request w/ JWT"] --> F["Token auth verify"] --> C
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Short-lived JWTs with exp + refresh flow
  • ➕ Limits exposure window without server-side state
  • ➕ Standard JWT practice; easier to reason about token lifetime
  • ➖ Requires refresh/re-issuance mechanism and client changes
  • ➖ Does not support immediate per-identity invalidation without additional state
2. JWT jti denylist / session table
  • ➕ Supports immediate revocation of specific tokens
  • ➕ More granular than per-identity cutoff
  • ➖ Introduces server-side storage and lookup on every request
  • ➖ Operational complexity (cleanup, indexing, potential hot paths)
3. Rotate signing keys (per-identity or global)
  • ➕ Immediate invalidation without per-request DB lookup if keys are cached per identity
  • ➕ Strong cryptographic boundary for rotation events
  • ➖ More complex key management and migration strategy
  • ➖ Harder to scope rotation to one identity without per-identity key material

Recommendation: Keep the token_min_iat cutoff approach as implemented: it provides immediate, per-identity invalidation with minimal new machinery and a migration-safe default (0) to avoid fleet lockout. Consider adding exp-based TTL later as a defense-in-depth follow-up, but token_min_iat is a pragmatic first step for incident response and rotation workflows.

Files changed (13) +1695 / -26

Enhancement (10) +949 / -26
agent_registry_store.pyAdd sponsor_contact_id and token_min_iat with migrations and store APIs +131/-15

Add sponsor_contact_id and token_min_iat with migrations and store APIs

• Extends the agent_registry schema with sponsor_contact_id and introduces idempotent migrations v5 (sponsor_contact_id) and v6 (token_min_iat default 0). Adds register() support for sponsor_contact_id plus new methods list_by_sponsor(), set_sponsor(), and bump_token_min_iat() used for delegation revocation and token rotation.

tinyagentos/agent_registry_store.py

agent_token_auth.pyEnforce token_min_iat cutoff during registry JWT verification +10/-2

Enforce token_min_iat cutoff during registry JWT verification

• Updates module documentation and adds an auth check rejecting JWTs whose iat is strictly less than the registry record’s token_min_iat, returning 401 token superseded while preserving existing status/grant checks.

tinyagentos/agent_token_auth.py

delegation_handler.pyAdd cross-user collab D1 delegation handler and kill-switch utilities +589/-0

Add cross-user collab D1 delegation handler and kill-switch utilities

• Introduces delegation request processing (validation, membership gate, denylist application, policy-based auto/manual approval), Decisions-based approval completion to mint invites, cascade sponsor revocation with task unassignment and optional A2A system messaging, and a per-instance peer-route panic switch.

tinyagentos/delegation_handler.py

invite_store.pyAdd metadata column to project_invites and propagate through mint/get +22/-5

Add metadata column to project_invites and propagate through mint/get

• Extends project_invites with a metadata JSON column (idempotent ALTER TABLE), updates mint() to accept/store metadata, and enhances row-to-dict conversion to deserialize scopes/metadata for consumers such as delegation and invite redemption.

tinyagentos/projects/invite_store.py

project_store.pyAdd project membership helper and JSON settings get/set APIs +58/-0

Add project membership helper and JSON settings get/set APIs

• Adds is_project_member() for membership checks (optionally constrained by member_kind) and get_project_setting()/set_project_setting() helpers used by delegation policy gating.

tinyagentos/projects/project_store.py

agent_auth_requests.pyThread sponsor_contact_id through agent approval/registration flow +10/-0

Thread sponsor_contact_id through agent approval/registration flow

• Extends approve_request_record() to accept sponsor_contact_id, sets sponsor on reused identities, and persists sponsor_contact_id on newly registered identities to support delegated agent sponsorship tracking.

tinyagentos/routes/agent_auth_requests.py

agent_registry.pyAdd owner/admin rotate-tokens endpoint with audit logging +35/-0

Add owner/admin rotate-tokens endpoint with audit logging

• Introduces POST /api/agents/registry/{id}/rotate-tokens that bumps token_min_iat to now, restricts access to owner/admin, returns 404 for missing identities, and emits a governance audit record (action rotate-tokens).

tinyagentos/routes/agent_registry.py

decisions.pyRoute collab delegation Decisions to invite-mint completion handler +58/-3

Route collab delegation Decisions to invite-mint completion handler

• Extends answer_decision() routing to include a new collab delegation side effect handler. Implements _apply_collab_delegation_grant() to mint a sponsored invite on approval and route informative replies back to the requesting peer without failing the persisted decision on side-effect errors.

tinyagentos/routes/decisions.py

peer.pyAdd peer panic switch gate and dispatch delegation_request envelopes +16/-1

Add peer panic switch gate and dispatch delegation_request envelopes

• Blocks peer traffic when the per-instance panic flag is active and adds dispatch logic to send delegation_request envelopes to process_delegation_request(), returning handler status in the inbox response.

tinyagentos/routes/peer.py

project_invites.pyPropagate sponsor_contact_id from invite metadata into agent creation +20/-0

Propagate sponsor_contact_id from invite metadata into agent creation

• Parses delegation-sponsored invite metadata to extract sponsor_contact_id and passes it through redeem flows into approve_request_record(), ensuring sponsored agents are marked in the registry during invite redemption.

tinyagentos/routes/project_invites.py

Tests (3) +746 / -0
test_agent_registry_store.pyAdd store tests for token_min_iat bump and defaults +35/-0

Add store tests for token_min_iat bump and defaults

• Adds a focused test class verifying bump_token_min_iat persists the cutoff, new registrations default to token_min_iat=0, and bumping a nonexistent canonical_id returns None.

tests/test_agent_registry_store.py

test_collab_d1_delegation.pyIntroduce D1 delegation tests (scope gates, sponsor APIs, invite metadata) +300/-0

Introduce D1 delegation tests (scope gates, sponsor APIs, invite metadata)

• Adds coverage for delegation scope denylist behavior, envelope body validation, sponsor listing/setting on registry records, idempotent sponsor migration, and metadata JSON round-tripping in project invites.

tests/test_collab_d1_delegation.py

test_token_rotation.pyAdd integration tests for per-identity token rotation and rotate-tokens route +411/-0

Add integration tests for per-identity token rotation and rotate-tokens route

• Introduces end-to-end tests covering store→auth-path→route behavior: old tokens are rejected after bump (401 token superseded), new tokens pass after bump, default-zero avoids lockout, and the rotate-tokens endpoint returns expected responses including 404 for unknown identities.

tests/test_token_rotation.py

Comment thread tinyagentos/delegation_handler.py Outdated

return {
"status": "approved",
"invite_id": invite["id"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: KeyError — invite["id"] should be invite["record"]["invite_id"]

invite_store.mint() returns {"record": {..., "invite_id": ...}, "pin": ...}. Accessing invite["id"] raises an unhandled KeyError, causing a 500 whenever auto-approve is enabled.

Suggested change
"invite_id": invite["id"],
"invite_id": invite["record"]["invite_id"],

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/delegation_handler.py Outdated

return {
"status": "approved",
"invite_id": invite["id"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: KeyError — invite["id"] should be invite["record"]["invite_id"]

Same typo in complete_delegation_approval. invite_store.mint() returns a dict with record.invite_id, not a top-level id key. This crashes the approval flow when a delegation decision is approved.

Suggested change
"invite_id": invite["id"],
invite_id = invite["record"]["invite_id"],

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tinyagentos/routes/peer.py (1)

258-273: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Dispatch branch sits below the "unrecognised kind" log.

Every delegation_request now logs unrecognised kind — accepted, no dispatch before being dispatched, which will mislead anyone debugging the peer channel. Move the branch above the log (and drop the duplicate body_data read from Line 250).

♻️ Proposed fix
+    # Dispatch delegation requests to the cross-user collab handler (D1).
+    if kind == "delegation_request":
+        from tinyagentos.delegation_handler import process_delegation_request
+
+        result = await process_delegation_request(
+            request, contact_id=contact_id, envelope_body=body_data,
+        )
+        return {"status": result.get("status", "unknown"), "detail": result}
+
     # Log unrecognised kinds for debugging; they are accepted but not dispatched.
     logger.info(
         "peer_inbox: contact=%s kind=%s nonce=%s (unrecognised kind — accepted, no dispatch)",
         contact_id, kind, envelope.get("nonce", "?"),
     )
 
-    # Dispatch delegation requests to the cross-user collab handler (D1).
-    if kind == "delegation_request":
-        from tinyagentos.delegation_handler import process_delegation_request
-
-        body_data = envelope.get("body", {})
-        result = await process_delegation_request(
-            request, contact_id=contact_id, envelope_body=body_data,
-        )
-        return {"status": result.get("status", "unknown"), "detail": result}
-
     return {"status": "received", "kind": kind, "nonce": envelope.get("nonce")}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/peer.py` around lines 258 - 273, Move the
delegation_request dispatch branch in the peer message handler above the
unrecognised-kind logger so recognized requests are not logged as unrecognised.
Reuse the existing body_data value from the earlier code instead of reading
envelope.get("body", {}) again, while preserving the process_delegation_request
call and response handling.
🧹 Nitpick comments (10)
tinyagentos/routes/decisions.py (1)

476-479: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the extraneous f prefix.

♻️ Proposed fix
-        error_msg = (
-            f"Delegation approval failed: an internal error occurred while "
-            f"setting up the sponsored agent. Please retry or check the logs."
-        )
+        error_msg = (
+            "Delegation approval failed: an internal error occurred while "
+            "setting up the sponsored agent. Please retry or check the logs."
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/decisions.py` around lines 476 - 479, Remove the
unnecessary f-string prefix from the error_msg assignment in the delegation
approval error-handling code, since the message contains no interpolated
expressions. Preserve the existing message text unchanged.

Source: Linters/SAST tools

tinyagentos/projects/project_store.py (1)

382-404: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the duplicated branches into one query.

♻️ Optional simplification
-        if member_kind is not None:
-            async with self._db.execute(
-                "SELECT 1 FROM project_members "
-                "WHERE project_id = ? AND member_id = ? AND member_kind = ?",
-                (project_id, member_id, member_kind),
-            ) as cur:
-                return (await cur.fetchone()) is not None
-        else:
-            async with self._db.execute(
-                "SELECT 1 FROM project_members "
-                "WHERE project_id = ? AND member_id = ?",
-                (project_id, member_id),
-            ) as cur:
-                return (await cur.fetchone()) is not None
+        sql = "SELECT 1 FROM project_members WHERE project_id = ? AND member_id = ?"
+        params: list = [project_id, member_id]
+        if member_kind is not None:
+            sql += " AND member_kind = ?"
+            params.append(member_kind)
+        async with self._db.execute(sql, params) as cur:
+            return (await cur.fetchone()) is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/projects/project_store.py` around lines 382 - 404, Update
is_project_member to use a single SQL query and parameter set, making the
member_kind predicate optional while preserving filtering by member_kind when
provided and matching any kind when omitted. Remove the duplicated execute
branches and retain the existing boolean result behavior.
tinyagentos/delegation_handler.py (2)

563-589: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Kill switch is process-local and not persisted.

app.state._peer_disabled resets on restart and is not shared across workers, so a level-3 panic silently lapses (or applies to only one worker under multi-process deployment). Consider persisting the flag (settings/DB) and reading it in is_peer_disabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/delegation_handler.py` around lines 563 - 589, The level-3 peer
disable state currently exists only in process-local app state. Update
kill_switch_per_instance, kill_switch_reenable, and is_peer_disabled to persist
the flag through the existing shared settings or database mechanism, and have
is_peer_disabled read that persisted value so the panic survives restarts and is
consistent across workers.

500-522: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silent swallow makes partial unassign undebuggable.

Line 521-522 drops every per-task failure with no log, so unassigned_count under-reports with no trace — exactly the forensic trail a kill-switch needs. Also except AttributeError only guards the call itself; an awaited coroutine raising AttributeError internally would be misrouted to the fallback.

♻️ Proposed fix
         try:
             await task_store.update_task(task_id, assignee_id=None, status="ready")
             count += 1
-        except Exception:
-            pass
+        except Exception:
+            logger.warning(
+                "cascade_revoke: failed to unassign task %s from %s",
+                task_id, canonical_id, exc_info=True,
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/delegation_handler.py` around lines 500 - 522, Update the
task-unassignment loop around task_store.update_task to log each per-task
exception with the task identifier before continuing, preserving the count
behavior for successful updates. Narrow the AttributeError handling around
task_store.list_for_assignee so only a missing-method condition triggers the
list_tasks fallback, while AttributeError raised during coroutine execution is
propagated or handled as a real failure rather than misrouted.

Source: Linters/SAST tools

tinyagentos/routes/project_invites.py (1)

1124-1131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract this metadata parsing (duplicated at Lines 1224-1231) and enforce a string sponsor id.

♻️ Proposed helper
def _sponsor_from_invite(invite: dict) -> str | None:
    meta = invite.get("metadata") or {}
    if isinstance(meta, str):
        try:
            meta = json.loads(meta)
        except (ValueError, TypeError):
            meta = {}
    if not isinstance(meta, dict):
        return None
    sponsor = meta.get("sponsor_contact_id")
    return sponsor if isinstance(sponsor, str) and sponsor.strip() else None
-    invite_metadata = invite.get("metadata") or {}
-    if isinstance(invite_metadata, str):
-        try:
-            invite_metadata = json.loads(invite_metadata)
-        except (ValueError, TypeError):
-            invite_metadata = {}
-    sponsor_contact_id = invite_metadata.get("sponsor_contact_id") if isinstance(invite_metadata, dict) else None
+    sponsor_contact_id = _sponsor_from_invite(invite)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/project_invites.py` around lines 1124 - 1131, The
duplicated sponsor metadata parsing in the invite handling paths should be
centralized and should only return valid non-empty string IDs. Add a helper such
as _sponsor_from_invite, replace both parsing blocks around the existing
extraction sites with calls to it, and preserve None for invalid, missing,
non-dictionary, malformed, or blank sponsor_contact_id values.
tests/test_collab_d1_delegation.py (1)

270-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the delegation mint paths.

The store/validation layers are covered, but _auto_approve_delegation and complete_delegation_approval (the code that actually reads the mint result and the project_invite_store app-state attribute) have no test. Both contain contract bugs flagged in tinyagentos/delegation_handler.py that a single test would surface.

Want me to draft those tests?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_collab_d1_delegation.py` around lines 270 - 300, The existing
TestInviteMetadata coverage only exercises ProjectInviteStore.mint directly; add
a test covering the delegation mint flow through _auto_approve_delegation and
complete_delegation_approval. Configure the app state’s project_invite_store,
invoke the delegation approval path, and assert it correctly consumes the mint
result and completes approval without contract errors, including the expected
invite/approval outcome.
tinyagentos/agent_registry_store.py (1)

845-863: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

bump_token_min_iat can lower the cutoff, silently un-superseding old tokens.

The UPDATE unconditionally sets token_min_iat = ? regardless of the current value. If this is ever called with a timestamp earlier than the existing cutoff (e.g., clock skew, a stale/duplicate rotation call, or a future caller other than the route), tokens that were already superseded become valid again — defeating the security guarantee this feature exists to provide.

🔒 Proposed fix: make the cutoff monotonically non-decreasing
         await self._db.execute(
-            "UPDATE agent_registry SET token_min_iat = ? WHERE canonical_id = ?",
+            "UPDATE agent_registry SET token_min_iat = MAX(token_min_iat, ?) WHERE canonical_id = ?",
             (ts, canonical_id),
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/agent_registry_store.py` around lines 845 - 863, Update
bump_token_min_iat so the database update only raises token_min_iat, never
lowers an existing cutoff; use a monotonic SQL condition or equivalent
comparison while preserving the existing missing-record and updated-record
behavior.
tests/test_token_rotation.py (2)

388-403: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused token bindings.

Static analysis flags token as unpacked-but-unused in both test_admin_can_rotate (Line 390) and test_owner_can_rotate (Line 399).

🧹 Proposed fix
-        cid, token = await _register_and_mint(app, user_id="admin")
+        cid, _token = await _register_and_mint(app, user_id="admin")

(apply to both occurrences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 388 - 403, Remove the unused token
bindings in both test_admin_can_rotate and test_owner_can_rotate by unpacking
only the cid value returned from _register_and_mint, while preserving the
existing rotation requests and assertions.

Source: Linters/SAST tools


22-236: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Large duplicated fixture instead of reusing the existing one.

The docstring states this fixture should Reuse the same init + admin-session logic as the main client fixture, but the ~200-line store-init/close sequence appears to be copy-pasted into this new file rather than depending on the existing fixture. If an equivalent fixture (e.g., the one backing the main API test client) already exists in conftest.py, depending on it directly and layering only the two extra agent_registry/agent_grants inits on top would avoid this duplication and its maintenance burden (every new store added to the app must now be duplicated here too).

#!/bin/bash
# Locate the fixture this docstring claims to mirror, to confirm duplication.
fd conftest.py --type f
rg -n "async def .*client" tests/conftest.py -A5 2>/dev/null | head -50
rg -n "def app\(" tests/conftest.py -A5 2>/dev/null | head -50
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 22 - 236, Refactor the agent_app
fixture to depend on the existing main client fixture from conftest.py,
preserving its initialized stores, admin session, and cleanup behavior. Remove
the duplicated store initialization and teardown sequence, and retain only the
agent_registry and agent_grants initialization needed by this fixture before
yielding the reused client.
tinyagentos/routes/agent_registry.py (1)

770-777: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Audit entry carries no information about the actual rotation.

_audit_governance is called with before_status/after_status both derived from status (unaffected by rotation), so the persisted audit record always shows before == after and doesn't capture what changed (the old vs. new token_min_iat). This weakens the "forensic audit-log entry" the PR objective calls for — an investigator can see that a rotation happened and who triggered it, but not the cutoff values involved.

Consider extending the audit payload (or _audit_governance) to include record["token_min_iat"] (before) and ts (after) instead of reusing the status fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/agent_registry.py` around lines 770 - 777, Update the
rotate-tokens audit flow around _audit_governance to record the actual cutoff
transition: use the pre-rotation record["token_min_iat"] as the before value and
ts as the after value, rather than deriving both from status. Extend the audit
payload or _audit_governance as needed while preserving the actor and rotation
context.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tinyagentos/delegation_handler.py`:
- Around line 294-298: Update both _auto_approve_delegation
(tinyagentos/delegation_handler.py#L294-L298) and complete_delegation_approval
(tinyagentos/delegation_handler.py#L378-L382) to read the minted invite
identifier from invite["record"]["invite_id"] instead of invite["id"],
preserving the existing response shape.
- Around line 264-266: Update the invite-store lookups in both
_auto_approve_delegation and complete_delegation_approval to read
request.app.state.project_invites instead of project_invite_store. Preserve the
existing unavailable-store error response and all surrounding delegation
behavior.
- Around line 416-420: Update the project-scoped filtering around project_id and
project_store so a set project_id without a project_store errors out immediately
instead of skipping membership checks. Preserve the existing is_project_member
filtering when project_store is available, preventing revocation of identities
outside the requested project.
- Around line 154-171: Restrict the auto-approval path in the delegation handler
around validate_delegation_scopes and _auto_approve_delegation to grant only
scopes in SPONSORED_DEFAULT_SCOPES; scopes outside that default tier must not be
auto-approved and must continue through explicit per-scope Decisions approval,
while preserving the existing hard-denial behavior.

In `@tinyagentos/projects/project_store.py`:
- Around line 405-417: Update get_project_setting to validate that the project’s
settings value is a dictionary before calling get on it. Return default when
settings is missing or any non-dict value, while preserving keyed lookup
behavior for valid dictionaries and matching the guard used by
set_project_setting.

In `@tinyagentos/routes/agent_auth_requests.py`:
- Around line 460-463: Update the existing-identity reuse branch around
registry.set_sponsor so sponsorship is assigned only when the identity currently
has no sponsor. Load or reuse the existing identity’s current sponsor value,
preserve any existing or different sponsor, and skip set_sponsor when it is
already populated.

In `@tinyagentos/routes/agent_registry.py`:
- Around line 761-778: Handle a None result from store.bump_token_min_iat in the
token-rotation flow before accessing updated.get or auditing; return the same
404 not-found response used when the initial store.get returns None, while
preserving the existing success path for a returned record.

---

Outside diff comments:
In `@tinyagentos/routes/peer.py`:
- Around line 258-273: Move the delegation_request dispatch branch in the peer
message handler above the unrecognised-kind logger so recognized requests are
not logged as unrecognised. Reuse the existing body_data value from the earlier
code instead of reading envelope.get("body", {}) again, while preserving the
process_delegation_request call and response handling.

---

Nitpick comments:
In `@tests/test_collab_d1_delegation.py`:
- Around line 270-300: The existing TestInviteMetadata coverage only exercises
ProjectInviteStore.mint directly; add a test covering the delegation mint flow
through _auto_approve_delegation and complete_delegation_approval. Configure the
app state’s project_invite_store, invoke the delegation approval path, and
assert it correctly consumes the mint result and completes approval without
contract errors, including the expected invite/approval outcome.

In `@tests/test_token_rotation.py`:
- Around line 388-403: Remove the unused token bindings in both
test_admin_can_rotate and test_owner_can_rotate by unpacking only the cid value
returned from _register_and_mint, while preserving the existing rotation
requests and assertions.
- Around line 22-236: Refactor the agent_app fixture to depend on the existing
main client fixture from conftest.py, preserving its initialized stores, admin
session, and cleanup behavior. Remove the duplicated store initialization and
teardown sequence, and retain only the agent_registry and agent_grants
initialization needed by this fixture before yielding the reused client.

In `@tinyagentos/agent_registry_store.py`:
- Around line 845-863: Update bump_token_min_iat so the database update only
raises token_min_iat, never lowers an existing cutoff; use a monotonic SQL
condition or equivalent comparison while preserving the existing missing-record
and updated-record behavior.

In `@tinyagentos/delegation_handler.py`:
- Around line 563-589: The level-3 peer disable state currently exists only in
process-local app state. Update kill_switch_per_instance, kill_switch_reenable,
and is_peer_disabled to persist the flag through the existing shared settings or
database mechanism, and have is_peer_disabled read that persisted value so the
panic survives restarts and is consistent across workers.
- Around line 500-522: Update the task-unassignment loop around
task_store.update_task to log each per-task exception with the task identifier
before continuing, preserving the count behavior for successful updates. Narrow
the AttributeError handling around task_store.list_for_assignee so only a
missing-method condition triggers the list_tasks fallback, while AttributeError
raised during coroutine execution is propagated or handled as a real failure
rather than misrouted.

In `@tinyagentos/projects/project_store.py`:
- Around line 382-404: Update is_project_member to use a single SQL query and
parameter set, making the member_kind predicate optional while preserving
filtering by member_kind when provided and matching any kind when omitted.
Remove the duplicated execute branches and retain the existing boolean result
behavior.

In `@tinyagentos/routes/agent_registry.py`:
- Around line 770-777: Update the rotate-tokens audit flow around
_audit_governance to record the actual cutoff transition: use the pre-rotation
record["token_min_iat"] as the before value and ts as the after value, rather
than deriving both from status. Extend the audit payload or _audit_governance as
needed while preserving the actor and rotation context.

In `@tinyagentos/routes/decisions.py`:
- Around line 476-479: Remove the unnecessary f-string prefix from the error_msg
assignment in the delegation approval error-handling code, since the message
contains no interpolated expressions. Preserve the existing message text
unchanged.

In `@tinyagentos/routes/project_invites.py`:
- Around line 1124-1131: The duplicated sponsor metadata parsing in the invite
handling paths should be centralized and should only return valid non-empty
string IDs. Add a helper such as _sponsor_from_invite, replace both parsing
blocks around the existing extraction sites with calls to it, and preserve None
for invalid, missing, non-dictionary, malformed, or blank sponsor_contact_id
values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 91988a57-fb4a-4d47-ba8c-18736de70c41

📥 Commits

Reviewing files that changed from the base of the PR and between 6dda472 and bbbf0ea.

📒 Files selected for processing (13)
  • tests/test_agent_registry_store.py
  • tests/test_collab_d1_delegation.py
  • tests/test_token_rotation.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/delegation_handler.py
  • tinyagentos/projects/invite_store.py
  • tinyagentos/projects/project_store.py
  • tinyagentos/routes/agent_auth_requests.py
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/routes/decisions.py
  • tinyagentos/routes/peer.py
  • tinyagentos/routes/project_invites.py

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +154 to +171
granted_scopes, denied_scopes = validate_delegation_scopes(requested_scopes)

# Check project policy: auto_approve_delegation knob.
auto_approve = await project_store.get_project_setting(
project_id, "auto_approve_delegation", default=False
)

if auto_approve:
# Future dev-swarm path: immediate approval.
return await _auto_approve_delegation(
request,
contact_id=contact_id,
agent_slug=agent_slug,
display_name=display_name,
granted_scopes=granted_scopes,
denied_scopes=denied_scopes,
project_id=project_id,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Auto-approve mints every non-denied scope, contradicting the documented tier.

validate_delegation_scopes only strips the two hard-denied scopes, and its own docstring (Lines 48-50) states scopes outside SPONSORED_DEFAULT_SCOPES "require explicit per-scope Decisions approval". On the auto_approve_delegation path no human sees them, so a remote contact can self-grant e.g. canvas_write or files_read by listing them in the envelope.

🔒 Proposed fix — restrict the auto path to the default tier
     if auto_approve:
         # Future dev-swarm path: immediate approval.
+        auto_scopes = sorted(set(granted_scopes) & SPONSORED_DEFAULT_SCOPES)
+        elevated = sorted(set(granted_scopes) - SPONSORED_DEFAULT_SCOPES)
+        if elevated:
+            logger.warning(
+                "delegation: auto-approve dropped elevated scopes %r for %s",
+                elevated, contact_id,
+            )
         return await _auto_approve_delegation(
             request,
             contact_id=contact_id,
             agent_slug=agent_slug,
             display_name=display_name,
-            granted_scopes=granted_scopes,
-            denied_scopes=denied_scopes,
+            granted_scopes=auto_scopes,
+            denied_scopes=denied_scopes + elevated,
             project_id=project_id,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
granted_scopes, denied_scopes = validate_delegation_scopes(requested_scopes)
# Check project policy: auto_approve_delegation knob.
auto_approve = await project_store.get_project_setting(
project_id, "auto_approve_delegation", default=False
)
if auto_approve:
# Future dev-swarm path: immediate approval.
return await _auto_approve_delegation(
request,
contact_id=contact_id,
agent_slug=agent_slug,
display_name=display_name,
granted_scopes=granted_scopes,
denied_scopes=denied_scopes,
project_id=project_id,
)
granted_scopes, denied_scopes = validate_delegation_scopes(requested_scopes)
# Check project policy: auto_approve_delegation knob.
auto_approve = await project_store.get_project_setting(
project_id, "auto_approve_delegation", default=False
)
if auto_approve:
# Future dev-swarm path: immediate approval.
auto_scopes = sorted(set(granted_scopes) & SPONSORED_DEFAULT_SCOPES)
elevated = sorted(set(granted_scopes) - SPONSORED_DEFAULT_SCOPES)
if elevated:
logger.warning(
"delegation: auto-approve dropped elevated scopes %r for %s",
elevated, contact_id,
)
return await _auto_approve_delegation(
request,
contact_id=contact_id,
agent_slug=agent_slug,
display_name=display_name,
granted_scopes=auto_scopes,
denied_scopes=denied_scopes + elevated,
project_id=project_id,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/delegation_handler.py` around lines 154 - 171, Restrict the
auto-approval path in the delegation handler around validate_delegation_scopes
and _auto_approve_delegation to grant only scopes in SPONSORED_DEFAULT_SCOPES;
scopes outside that default tier must not be auto-approved and must continue
through explicit per-scope Decisions approval, while preserving the existing
hard-denial behavior.

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +264 to +266
invite_store = getattr(request.app.state, "project_invite_store", None)
if invite_store is None:
return {"status": "error", "error": "invite store not available"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Wrong app.state attribute — the invite store is project_invites.

Everywhere else in the codebase the store is registered as request.app.state.project_invites (routes/peer.py Line 511, routes/project_invites.py Line 1092, and the test fixtures). project_invite_store is never set, so both _auto_approve_delegation and complete_delegation_approval (Line 340) always short-circuit with "invite store not available" — delegation can never complete.

🐛 Proposed fix (apply at both sites)
-    invite_store = getattr(request.app.state, "project_invite_store", None)
+    invite_store = getattr(request.app.state, "project_invites", None)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
invite_store = getattr(request.app.state, "project_invite_store", None)
if invite_store is None:
return {"status": "error", "error": "invite store not available"}
invite_store = getattr(request.app.state, "project_invites", None)
if invite_store is None:
return {"status": "error", "error": "invite store not available"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/delegation_handler.py` around lines 264 - 266, Update the
invite-store lookups in both _auto_approve_delegation and
complete_delegation_approval to read request.app.state.project_invites instead
of project_invite_store. Preserve the existing unavailable-store error response
and all surrounding delegation behavior.

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +294 to +298
return {
"status": "approved",
"invite_id": invite["id"],
"agent_slug": agent_slug,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

mint return-shape mismatch at both delegation mint sites. ProjectInviteStore.mint returns {"record": {..., "invite_id": ...}, "pin": ...}, so invite["id"] raises KeyError in both handlers; both reads sit outside the surrounding try, so the failure escapes as a 500 / generic "approval failed".

  • tinyagentos/delegation_handler.py#L294-L298: in _auto_approve_delegation, return invite["record"]["invite_id"].
  • tinyagentos/delegation_handler.py#L378-L382: in complete_delegation_approval, return invite["record"]["invite_id"].
📍 Affects 1 file
  • tinyagentos/delegation_handler.py#L294-L298 (this comment)
  • tinyagentos/delegation_handler.py#L378-L382
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/delegation_handler.py` around lines 294 - 298, Update both
_auto_approve_delegation (tinyagentos/delegation_handler.py#L294-L298) and
complete_delegation_approval (tinyagentos/delegation_handler.py#L378-L382) to
read the minted invite identifier from invite["record"]["invite_id"] instead of
invite["id"], preserving the existing response shape.

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +416 to +420
if project_id is not None and project_store is not None:
if not await project_store.is_project_member(
project_id, canonical_id
):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Project-scoped revoke fails open when project_store is missing.

With project_id set but no project_store on app.state, the membership filter is skipped and every sponsored identity for the contact is revoked — a wider blast radius than the caller asked for. Prefer erroring out instead.

🛡️ Proposed fix
+    if project_id is not None and project_store is None:
+        return {"status": "error", "error": "project store not available"}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if project_id is not None and project_store is not None:
if not await project_store.is_project_member(
project_id, canonical_id
):
continue
if project_id is not None and project_store is None:
return {"status": "error", "error": "project store not available"}
if project_id is not None and project_store is not None:
if not await project_store.is_project_member(
project_id, canonical_id
):
continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/delegation_handler.py` around lines 416 - 420, Update the
project-scoped filtering around project_id and project_store so a set project_id
without a project_store errors out immediately instead of skipping membership
checks. Preserve the existing is_project_member filtering when project_store is
available, preventing revocation of identities outside the requested project.

Comment thread tinyagentos/projects/project_store.py Outdated
Comment on lines +405 to +417
async def get_project_setting(
self, project_id: str, key: str, default=None
):
"""Read a single key from the project's JSON settings dict.

Returns *default* if the project does not exist, has no settings, or
the key is absent.
"""
project = await self.get_project(project_id)
if project is None:
return default
settings = project.get("settings") or {}
return settings.get(key, default)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against a non-dict settings here too.

set_project_setting defends with isinstance(settings, dict) (Line 430) but the reader does not; a legacy row whose settings JSON is a list/scalar makes this raise AttributeError instead of returning default.

🛡️ Proposed fix
-        settings = project.get("settings") or {}
-        return settings.get(key, default)
+        settings = project.get("settings") or {}
+        if not isinstance(settings, dict):
+            return default
+        return settings.get(key, default)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def get_project_setting(
self, project_id: str, key: str, default=None
):
"""Read a single key from the project's JSON settings dict.
Returns *default* if the project does not exist, has no settings, or
the key is absent.
"""
project = await self.get_project(project_id)
if project is None:
return default
settings = project.get("settings") or {}
return settings.get(key, default)
async def get_project_setting(
self, project_id: str, key: str, default=None
):
"""Read a single key from the project's JSON settings dict.
Returns *default* if the project does not exist, has no settings, or
the key is absent.
"""
project = await self.get_project(project_id)
if project is None:
return default
settings = project.get("settings") or {}
if not isinstance(settings, dict):
return default
return settings.get(key, default)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/projects/project_store.py` around lines 405 - 417, Update
get_project_setting to validate that the project’s settings value is a
dictionary before calling get on it. Return default when settings is missing or
any non-dict value, while preserving keyed lookup behavior for valid
dictionaries and matching the guard used by set_project_setting.

Comment on lines +460 to +463
# For sponsored agents (cross-user collab D1), set the sponsor
# on the existing identity when reusing it for a new project.
if sponsor_contact_id:
await registry.set_sponsor(existing_cid, sponsor_contact_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reusing an existing handle can retro-assign sponsorship of a pre-existing identity.

This branch fires when the derived handle already maps to an ACTIVE identity that may have been created by a normal consent flow (or sponsored by a different contact). Setting sponsor_contact_id unconditionally brings that identity under the new contact's cascade_sponsor_revoke authority and overwrites a previous sponsor. Only set it when the row currently has no sponsor.

🔒 Proposed fix
-            if sponsor_contact_id:
-                await registry.set_sponsor(existing_cid, sponsor_contact_id)
+            if sponsor_contact_id and not existing_active.get("sponsor_contact_id"):
+                await registry.set_sponsor(existing_cid, sponsor_contact_id)
+            elif sponsor_contact_id and existing_active.get("sponsor_contact_id") != sponsor_contact_id:
+                logger.warning(
+                    "auth-approve: refusing to re-sponsor %s (existing sponsor %r)",
+                    existing_cid, existing_active.get("sponsor_contact_id"),
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# For sponsored agents (cross-user collab D1), set the sponsor
# on the existing identity when reusing it for a new project.
if sponsor_contact_id:
await registry.set_sponsor(existing_cid, sponsor_contact_id)
# For sponsored agents (cross-user collab D1), set the sponsor
# on the existing identity when reusing it for a new project.
if sponsor_contact_id and not existing_active.get("sponsor_contact_id"):
await registry.set_sponsor(existing_cid, sponsor_contact_id)
elif sponsor_contact_id and existing_active.get("sponsor_contact_id") != sponsor_contact_id:
logger.warning(
"auth-approve: refusing to re-sponsor %s (existing sponsor %r)",
existing_cid, existing_active.get("sponsor_contact_id"),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/agent_auth_requests.py` around lines 460 - 463, Update the
existing-identity reuse branch around registry.set_sponsor so sponsorship is
assigned only when the identity currently has no sponsor. Load or reuse the
existing identity’s current sponsor value, preserve any existing or different
sponsor, and skip set_sponsor when it is already populated.

Comment thread tinyagentos/routes/agent_registry.py
Comment thread tinyagentos/routes/peer.py Outdated
from tinyagentos.delegation_handler import process_delegation_request

body_data = envelope.get("body", {})
result = await process_delegation_request(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Misleading log — delegation_request is dispatched but logged as "unrecognised kind"

The logger.info block above marks the kind as unrecognised before the delegation_request dispatch below. This masks real unrecognised kinds in logs.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • tests/test_token_rotation.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/routes/agent_registry.py
Previous Review Summary (commit bbbf0ea)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit bbbf0ea)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
tinyagentos/delegation_handler.py 296 invite["id"] KeyError — should be invite["record"]["invite_id"]
tinyagentos/delegation_handler.py 380 invite["id"] KeyError — should be invite["record"]["invite_id"]

WARNING

File Line Issue
tinyagentos/routes/peer.py 269 delegation_request is dispatched but logged as "unrecognised kind" — the logger.info block above labels it unrecognised despite having a dedicated handler just below
Files Reviewed (13 files)
  • tests/test_agent_registry_store.py
  • tests/test_collab_d1_delegation.py
  • tests/test_token_rotation.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/delegation_handler.py
  • tinyagentos/projects/invite_store.py
  • tinyagentos/projects/project_store.py
  • tinyagentos/routes/agent_auth_requests.py
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/routes/decisions.py
  • tinyagentos/routes/peer.py
  • tinyagentos/routes/project_invites.py

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 95.4K · Output: 21.5K · Cached: 473.1K

@qodo-code-review

qodo-code-review Bot commented Jul 29, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (2)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. metadata in invites SCHEMA ✓ Resolved 📜 Skill insight ≡ Correctness
Description
ProjectInviteStore.SCHEMA includes metadata, but _post_init() also conditionally adds
metadata via ALTER TABLE. This violates the rule that migration-added columns must not appear in
SCHEMA.
Code

tinyagentos/projects/invite_store.py[R35-37]

+    contact_id           TEXT,
+    metadata             TEXT NOT NULL DEFAULT '{}'
);
Relevance

●●● Strong

Same SCHEMA vs guarded ALTER TABLE conflict; checklist/initialization-order risk on older DBs.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires that columns introduced via migrations (including guarded ALTER TABLE in
_post_init) must not be present in SCHEMA. The PR adds metadata to the SCHEMA DDL and also
adds it via an _post_init() guard when missing.

tinyagentos/projects/invite_store.py[17-38]
tinyagentos/projects/invite_store.py[109-118]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ProjectInviteStore.SCHEMA` references `metadata`, but `_post_init()` adds `metadata` for older DBs via guarded `ALTER TABLE`. Compliance requires that `SCHEMA` not reference any migration-added columns.

## Issue Context
`SCHEMA` executes before `_post_init()` runs, so it must stay compatible with pre-migration DB assumptions.

## Fix Focus Areas
- tinyagentos/projects/invite_store.py[17-38]
- tinyagentos/projects/invite_store.py[114-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Delegation invite mint broken 🐞 Bug ≡ Correctness
Description
delegation_handler looks up the invite store at app.state.project_invite_store and returns
invite['id'], but the app wires the store as app.state.project_invites and
ProjectInviteStore.mint() returns {record:{invite_id,...}, pin}. As a result, delegation
auto-approve and approved Decisions side-effects will fail at runtime (either always “invite store
not available” or raising KeyError).
Code

tinyagentos/delegation_handler.py[R264-298]

+    invite_store = getattr(request.app.state, "project_invite_store", None)
+    if invite_store is None:
+        return {"status": "error", "error": "invite store not available"}
+
+    # The project-scoped scopes require a non-null project_id for the JWT.
+    project_scopes = sorted(set(granted_scopes) & _PROJECT_SCOPES)
+    non_project_scopes = sorted(set(granted_scopes) - _PROJECT_SCOPES)
+
+    try:
+        invite = await invite_store.mint(
+            project_id=project_id,
+            scopes=non_project_scopes + project_scopes,
+            approval_mode="auto",
+            check_interval_secs=1800,
+            created_by=contact_id,
+            metadata={
+                "kind": "delegation_sponsored",
+                "sponsor_contact_id": contact_id,
+                "agent_slug": agent_slug,
+                "display_name": display_name,
+                "pin_required": False,
+            },
+        )
+    except Exception:
+        logger.warning(
+            "delegation: failed to create invite for %s / %s",
+            contact_id, agent_slug, exc_info=True,
+        )
+        return {"status": "error", "error": "failed to create project invite"}
+
+    return {
+        "status": "approved",
+        "invite_id": invite["id"],
+        "agent_slug": agent_slug,
+    }
Relevance

●●● Strong

Clear runtime break (wrong app.state attr + wrong return shape); high-priority correctness fix.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The delegation handler uses a state attribute name that doesn’t exist in the app and then reads an
invite id field that ProjectInviteStore.mint() doesn’t return.

tinyagentos/delegation_handler.py[249-298]
tinyagentos/app.py[1596-1598]
tinyagentos/projects/invite_store.py[135-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`_auto_approve_delegation()` / `complete_delegation_approval()` are incompatible with the actual app wiring and `ProjectInviteStore.mint()` return shape.

### Issue Context
- The app sets the invite store on `app.state.project_invites`.
- `ProjectInviteStore.mint()` returns a dict with the invite id at `result["record"]["invite_id"]`.

### Fix Focus Areas
- tinyagentos/delegation_handler.py[249-299]
- tinyagentos/delegation_handler.py[340-383]

### Suggested change
- Replace `getattr(request.app.state, "project_invite_store", None)` with `getattr(request.app.state, "project_invites", None)`.
- When minting, treat the return value as `mint_result` and return `mint_result["record"]["invite_id"]` (and optionally return the `pin` if the flow needs it).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Delegation scopes can crash 🐞 Bug ☼ Reliability
Description
validate_delegation_scopes() / _validate_delegation_envelope_body() build
set(requested_scopes) without validating that every element is a string, so a peer can send an
unhashable element (e.g. an object) and trigger a TypeError -> 500. This is a new externally
reachable crash path via /api/peer/inbox delegation dispatch.
Code

tinyagentos/delegation_handler.py[R56-59]

+    requested_set = set(requested_scopes)
+    denied = sorted(requested_set & SPONSORED_DENY_SCOPES)
+    allowed = sorted(requested_set - SPONSORED_DENY_SCOPES)
+    if denied:
Relevance

●●● Strong

Input-hardening to avoid externally-triggered 500s; team has accepted similar validation guards.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler converts requested_scopes into a set without per-element validation, and the peer
inbox routes delegation envelopes into this handler.

tinyagentos/delegation_handler.py[42-104]
tinyagentos/routes/peer.py[264-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Delegation request validation checks only that `requested_scopes` is a list, but it does not validate list *elements* before using `set(...)`. Malformed JSON can trigger `TypeError: unhashable type` and crash the request.

### Issue Context
`/api/peer/inbox` dispatches `kind == "delegation_request"` directly into `process_delegation_request()`, so this error is reachable from authenticated peer traffic.

### Fix Focus Areas
- tinyagentos/delegation_handler.py[42-65]
- tinyagentos/delegation_handler.py[67-105]
- tinyagentos/routes/peer.py[264-273]

### Suggested change
- In `_validate_delegation_envelope_body`, validate that every entry in `requested_scopes` is a non-empty `str` (e.g., `all(isinstance(s, str) and s.strip() for s in value)`), otherwise return `(False, "requested_scopes must be a list of non-empty strings", None)`.
- Optionally defensively wrap `validate_delegation_scopes`’s `set(...)` conversion in a try/except and return a structured error if validation is bypassed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Superseded token still authorizes 🐞 Bug ⛨ Security
Description
The new token_min_iat cutoff is enforced only inside _verify_agent_scope, but
check_agent_identity() (used to authorize the scope-request creation flow) does not enforce
token_min_iat. After rotating tokens, an old token can still authenticate scope-request creation
for that identity.
Code

tinyagentos/agent_token_auth.py[R115-119]

+    # Reject tokens issued before the identity's token_min_iat cutoff (rotation).
+    token_min_iat = record.get("token_min_iat") or 0
+    token_iat = payload.get("iat") or 0
+    if token_iat < token_min_iat:
+        raise HTTPException(status_code=401, detail="token superseded")
Relevance

●●● Strong

Security hole: rotation cutoff must apply to identity auth too; likely to be fixed consistently.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
check_agent_identity() verifies signature and active status only, and it is used as an
authorization mechanism for creating scope requests; the cutoff check exists only in
_verify_agent_scope().

tinyagentos/agent_token_auth.py[83-120]
tinyagentos/agent_token_auth.py[159-198]
tinyagentos/routes/agent_auth_requests.py[919-927]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Token rotation is implemented in `_verify_agent_scope()`, but other token verification paths still accept superseded tokens.

### Issue Context
`create_scope_request` authorizes via `check_agent_identity()` for agent self-auth, and `check_agent_identity()` currently does not check `token_min_iat`.

### Fix Focus Areas
- tinyagentos/agent_token_auth.py[83-120]
- tinyagentos/agent_token_auth.py[159-198]
- tinyagentos/routes/agent_auth_requests.py[905-929]

### Suggested change
- Add the same `token_min_iat` vs `payload["iat"]` check to `check_agent_identity()`.
- Preferably refactor into a shared helper that:
 - verifies signature
 - loads registry record
 - checks status == active
 - enforces token_min_iat cutoff
 and is used by both `_verify_agent_scope()` and `check_agent_identity()` to prevent future drift.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. sponsor_contact_id in SCHEMA ✓ Resolved 📜 Skill insight ≡ Correctness
Description
agent_registry_store.SCHEMA includes sponsor_contact_id, but this column is also added via a
migration function. This violates the rule that migration-added columns must not be referenced in
SCHEMA, which can break initialization order guarantees on older DBs.
Code

tinyagentos/agent_registry_store.py[R48-49]

+    status              TEXT    NOT NULL DEFAULT 'active',
+    sponsor_contact_id  TEXT
Relevance

●●● Strong

Matches repo checklist: migration-added columns shouldn’t appear in SCHEMA; order/init safety issue.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids referencing migration-added columns in SCHEMA. The agent_registry table
SCHEMA includes sponsor_contact_id, while the PR also adds
_migration_v5_add_sponsor_contact_id() that conditionally `ALTER TABLE ... ADD COLUMN
sponsor_contact_id`, making it a migration-added column.

tinyagentos/agent_registry_store.py[35-50]
tinyagentos/agent_registry_store.py[209-226]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SCHEMA` currently includes `sponsor_contact_id`, while `_migration_v5_add_sponsor_contact_id()` also adds it. Per compliance, migration-added columns must not appear in `SCHEMA`.

## Issue Context
`BaseStore` runs `SCHEMA` before `_post_init()` migrations; `SCHEMA` must therefore only contain columns guaranteed to exist pre-migration.

## Fix Focus Areas
- tinyagentos/agent_registry_store.py[35-50]
- tinyagentos/agent_registry_store.py[209-227]
- tinyagentos/agent_registry_store.py[460-464]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Em dashes in docstrings 📜 Skill insight ✧ Quality
Description
New/modified docstrings and comments include Unicode em dashes (). This violates the no-em-dashes
rule for public-facing text and developer-visible strings.
Code

tinyagentos/delegation_handler.py[R1-8]

+"""Agent delegation handler — cross-user collab D1.
+
+Processes delegation-request envelopes from remote contacts, applies
+scope denylist and policy gates, creates Decisions cards for manual
+approval, and on approval mints project invites for the sponsored agent.
+
+Also provides cascade revocation and kill-switch machinery.
+"""
Relevance

●●● Strong

Trivial text-only compliance fix (replace em dashes); usually accepted when a checklist rule exists.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids em dashes in public-facing or developer-visible text. The PR adds em dashes in
new/modified docstrings/comments (e.g., Agent delegation handler — ..., and the updated registry
token auth docstring).

tinyagentos/delegation_handler.py[1-8]
tinyagentos/agent_token_auth.py[20-24]
tinyagentos/routes/agent_registry.py[754-760]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces em dash characters (`—`, U+2014) in docstrings/comments/strings, which are disallowed.

## Issue Context
Compliance requires using regular hyphens (`-`) or rephrasing instead of em dashes in any public-facing/developer-facing text.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[1-23]
- tinyagentos/agent_token_auth.py[20-24]
- tinyagentos/routes/agent_registry.py[754-760]
- tests/test_collab_d1_delegation.py[1-1]
- tests/test_agent_registry_store.py[1188-1190]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. rotate-tokens None deref ✓ Resolved 🐞 Bug ☼ Reliability
Description
rotate_tokens assumes bump_token_min_iat always returns a dict and immediately calls
updated.get(...), but the store method explicitly returns None when the record is missing. If the
record disappears between the initial get() and the update, this turns a race into a 500.
Code

tinyagentos/routes/agent_registry.py[R767-777]

+    ts = int(time.time())
+    updated = await store.bump_token_min_iat(canonical_id, ts)
+
+    await _audit_governance(
+        request,
+        action="rotate-tokens",
+        canonical_id=canonical_id,
+        actor_user_id=user.user_id,
+        before_status=record.get("status") or "active",
+        after_status=updated.get("status") or "active",
+    )
Relevance

●●● Strong

Obvious None-deref race -> 500; team commonly accepts defensive None handling in routes.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The route dereferences updated unconditionally, while the store method can return None on a
missing canonical_id.

tinyagentos/routes/agent_registry.py[761-777]
tinyagentos/agent_registry_store.py[845-862]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`rotate_tokens` does not handle `updated is None` and will raise when dereferencing it.

### Issue Context
`AgentRegistryStore.bump_token_min_iat()` does its own existence check and returns `None` when the record does not exist.

### Fix Focus Areas
- tinyagentos/routes/agent_registry.py[761-778]
- tinyagentos/agent_registry_store.py[845-862]

### Suggested change
- After calling `bump_token_min_iat`, handle `updated is None` with a 404 (or 409) and skip auditing.
- Optionally make the store update atomic (single UPDATE with rowcount check) so the route doesn’t need a pre-read + second read.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Same-second rotation loophole 🐞 Bug ⛨ Security
Description
rotate-tokens stores token_min_iat = int(time.time()) and tokens are minted with `iat =
int(time.time()); with the strict <` comparison, tokens minted earlier in the same second as the
bump have iat == token_min_iat and remain valid. Rotation therefore does not reliably invalidate
existing tokens without waiting for the next second boundary.
Code

tinyagentos/routes/agent_registry.py[R767-768]

+    ts = int(time.time())
+    updated = await store.bump_token_min_iat(canonical_id, ts)
Relevance

●● Moderate

Subtle same-second edge case; security-relevant but may be deemed acceptable for “smallest
mechanism”.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The route and minting both truncate to integer seconds, and the auth check only rejects
strictly-less iat values, so same-second tokens are unaffected.

tinyagentos/routes/agent_registry.py[748-778]
tinyagentos/agent_token_auth.py[115-120]
tinyagentos/agent_registry_store.py[322-369]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Token rotation compares second-resolution timestamps and uses a strict `<` comparison, leaving a same-second window where “old” tokens are not invalidated.

### Issue Context
- `mint_registry_token()` sets `iat` as `int(time.time())`.
- `rotate_tokens()` bumps `token_min_iat` to `int(time.time())`.
- Auth rejects only when `token_iat < token_min_iat`.

### Fix Focus Areas
- tinyagentos/routes/agent_registry.py[748-778]
- tinyagentos/agent_token_auth.py[115-120]
- tinyagentos/agent_registry_store.py[322-369]

### Suggested change (one viable approach)
- Switch `iat` and `token_min_iat` to higher precision values:
 - set `iat` to `time.time()` (float seconds)
 - set rotation cutoff `ts` to `time.time()` (float seconds)
 - keep the strict `<` comparison

This avoids same-second collisions while keeping NumericDate semantics (seconds since epoch).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

9. rotate_tokens lacks Pydantic response 📜 Skill insight ✧ Quality
Description
The new POST /api/agents/registry/{canonical_id}/rotate-tokens handler returns an untyped
dict/JSON without a declared Pydantic response model. This reduces schema validation and
documentation quality compared to the required Pydantic-model approach.
Code

tinyagentos/routes/agent_registry.py[R748-778]

+@router.post("/api/agents/registry/{canonical_id}/rotate-tokens")
+async def rotate_tokens(
+    request: Request,
+    canonical_id: str,
+    user: CurrentUser = Depends(current_user),
+):
+    """Bump ``token_min_iat`` to the current Unix timestamp, invalidating every
+    token minted before now for this identity.
+
+    Session owner or admin only.  The rotation is a single-write DB bump (no
+    new token is minted — the caller re-mints after).  Leaves a forensic
+    audit-log entry so every rotation is traceable to an actor.
+    """
+    store = _get_store(request)
+    record = await store.get(canonical_id)
+    if record is None:
+        return JSONResponse({"error": "not found"}, status_code=404)
+    require_owner_or_admin(user, record["user_id"])
+
+    ts = int(time.time())
+    updated = await store.bump_token_min_iat(canonical_id, ts)
+
+    await _audit_governance(
+        request,
+        action="rotate-tokens",
+        canonical_id=canonical_id,
+        actor_user_id=user.user_id,
+        before_status=record.get("status") or "active",
+        after_status=updated.get("status") or "active",
+    )
+    return updated
Relevance

● Weak

Team recently rejected adding response_model/Pydantic typing for route responses as “compliance”
refactor.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires Pydantic models for route payloads. The newly added route handler
rotate_tokens returns JSONResponse({...}) on 404 and otherwise returns updated (a raw dict)
without a response_model= declaration.

tinyagentos/routes/agent_registry.py[748-778]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `rotate_tokens` route does not declare a `response_model` and returns a raw dict/JSON response.

## Issue Context
Compliance requires route request/response payloads to use Pydantic models to enforce validation and produce correct OpenAPI.

## Fix Focus Areas
- tinyagentos/routes/agent_registry.py[748-778]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread tinyagentos/agent_registry_store.py Outdated
Comment thread tinyagentos/projects/invite_store.py Outdated
Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +1 to +8
"""Agent delegation handler — cross-user collab D1.

Processes delegation-request envelopes from remote contacts, applies
scope denylist and policy gates, creates Decisions cards for manual
approval, and on approval mints project invites for the sponsored agent.

Also provides cascade revocation and kill-switch machinery.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Em dashes in docstrings 📜 Skill insight ✧ Quality

New/modified docstrings and comments include Unicode em dashes (). This violates the no-em-dashes
rule for public-facing text and developer-visible strings.
Agent Prompt
## Issue description
The PR introduces em dash characters (`—`, U+2014) in docstrings/comments/strings, which are disallowed.

## Issue Context
Compliance requires using regular hyphens (`-`) or rephrasing instead of em dashes in any public-facing/developer-facing text.

## Fix Focus Areas
- tinyagentos/delegation_handler.py[1-23]
- tinyagentos/agent_token_auth.py[20-24]
- tinyagentos/routes/agent_registry.py[754-760]
- tests/test_collab_d1_delegation.py[1-1]
- tests/test_agent_registry_store.py[1188-1190]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +264 to +298
invite_store = getattr(request.app.state, "project_invite_store", None)
if invite_store is None:
return {"status": "error", "error": "invite store not available"}

# The project-scoped scopes require a non-null project_id for the JWT.
project_scopes = sorted(set(granted_scopes) & _PROJECT_SCOPES)
non_project_scopes = sorted(set(granted_scopes) - _PROJECT_SCOPES)

try:
invite = await invite_store.mint(
project_id=project_id,
scopes=non_project_scopes + project_scopes,
approval_mode="auto",
check_interval_secs=1800,
created_by=contact_id,
metadata={
"kind": "delegation_sponsored",
"sponsor_contact_id": contact_id,
"agent_slug": agent_slug,
"display_name": display_name,
"pin_required": False,
},
)
except Exception:
logger.warning(
"delegation: failed to create invite for %s / %s",
contact_id, agent_slug, exc_info=True,
)
return {"status": "error", "error": "failed to create project invite"}

return {
"status": "approved",
"invite_id": invite["id"],
"agent_slug": agent_slug,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Delegation invite mint broken 🐞 Bug ≡ Correctness

delegation_handler looks up the invite store at app.state.project_invite_store and returns
invite['id'], but the app wires the store as app.state.project_invites and
ProjectInviteStore.mint() returns {record:{invite_id,...}, pin}. As a result, delegation
auto-approve and approved Decisions side-effects will fail at runtime (either always “invite store
not available” or raising KeyError).
Agent Prompt
### Issue description
`_auto_approve_delegation()` / `complete_delegation_approval()` are incompatible with the actual app wiring and `ProjectInviteStore.mint()` return shape.

### Issue Context
- The app sets the invite store on `app.state.project_invites`.
- `ProjectInviteStore.mint()` returns a dict with the invite id at `result["record"]["invite_id"]`.

### Fix Focus Areas
- tinyagentos/delegation_handler.py[249-299]
- tinyagentos/delegation_handler.py[340-383]

### Suggested change
- Replace `getattr(request.app.state, "project_invite_store", None)` with `getattr(request.app.state, "project_invites", None)`.
- When minting, treat the return value as `mint_result` and return `mint_result["record"]["invite_id"]` (and optionally return the `pin` if the flow needs it).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/delegation_handler.py Outdated
Comment on lines +56 to +59
requested_set = set(requested_scopes)
denied = sorted(requested_set & SPONSORED_DENY_SCOPES)
allowed = sorted(requested_set - SPONSORED_DENY_SCOPES)
if denied:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

6. Delegation scopes can crash 🐞 Bug ☼ Reliability

validate_delegation_scopes() / _validate_delegation_envelope_body() build
set(requested_scopes) without validating that every element is a string, so a peer can send an
unhashable element (e.g. an object) and trigger a TypeError -> 500. This is a new externally
reachable crash path via /api/peer/inbox delegation dispatch.
Agent Prompt
### Issue description
Delegation request validation checks only that `requested_scopes` is a list, but it does not validate list *elements* before using `set(...)`. Malformed JSON can trigger `TypeError: unhashable type` and crash the request.

### Issue Context
`/api/peer/inbox` dispatches `kind == "delegation_request"` directly into `process_delegation_request()`, so this error is reachable from authenticated peer traffic.

### Fix Focus Areas
- tinyagentos/delegation_handler.py[42-65]
- tinyagentos/delegation_handler.py[67-105]
- tinyagentos/routes/peer.py[264-273]

### Suggested change
- In `_validate_delegation_envelope_body`, validate that every entry in `requested_scopes` is a non-empty `str` (e.g., `all(isinstance(s, str) and s.strip() for s in value)`), otherwise return `(False, "requested_scopes must be a list of non-empty strings", None)`.
- Optionally defensively wrap `validate_delegation_scopes`’s `set(...)` conversion in a try/except and return a structured error if validation is bypassed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +115 to +119
# Reject tokens issued before the identity's token_min_iat cutoff (rotation).
token_min_iat = record.get("token_min_iat") or 0
token_iat = payload.get("iat") or 0
if token_iat < token_min_iat:
raise HTTPException(status_code=401, detail="token superseded")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

7. Superseded token still authorizes 🐞 Bug ⛨ Security

The new token_min_iat cutoff is enforced only inside _verify_agent_scope, but
check_agent_identity() (used to authorize the scope-request creation flow) does not enforce
token_min_iat. After rotating tokens, an old token can still authenticate scope-request creation
for that identity.
Agent Prompt
### Issue description
Token rotation is implemented in `_verify_agent_scope()`, but other token verification paths still accept superseded tokens.

### Issue Context
`create_scope_request` authorizes via `check_agent_identity()` for agent self-auth, and `check_agent_identity()` currently does not check `token_min_iat`.

### Fix Focus Areas
- tinyagentos/agent_token_auth.py[83-120]
- tinyagentos/agent_token_auth.py[159-198]
- tinyagentos/routes/agent_auth_requests.py[905-929]

### Suggested change
- Add the same `token_min_iat` vs `payload["iat"]` check to `check_agent_identity()`.
- Preferably refactor into a shared helper that:
  - verifies signature
  - loads registry record
  - checks status == active
  - enforces token_min_iat cutoff
  and is used by both `_verify_agent_scope()` and `check_agent_identity()` to prevent future drift.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +767 to +768
ts = int(time.time())
updated = await store.bump_token_min_iat(canonical_id, ts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

8. Same-second rotation loophole 🐞 Bug ⛨ Security

rotate-tokens stores token_min_iat = int(time.time()) and tokens are minted with `iat =
int(time.time()); with the strict <` comparison, tokens minted earlier in the same second as the
bump have iat == token_min_iat and remain valid. Rotation therefore does not reliably invalidate
existing tokens without waiting for the next second boundary.
Agent Prompt
### Issue description
Token rotation compares second-resolution timestamps and uses a strict `<` comparison, leaving a same-second window where “old” tokens are not invalidated.

### Issue Context
- `mint_registry_token()` sets `iat` as `int(time.time())`.
- `rotate_tokens()` bumps `token_min_iat` to `int(time.time())`.
- Auth rejects only when `token_iat < token_min_iat`.

### Fix Focus Areas
- tinyagentos/routes/agent_registry.py[748-778]
- tinyagentos/agent_token_auth.py[115-120]
- tinyagentos/agent_registry_store.py[322-369]

### Suggested change (one viable approach)
- Switch `iat` and `token_min_iat` to higher precision values:
  - set `iat` to `time.time()` (float seconds)
  - set rotation cutoff `ts` to `time.time()` (float seconds)
  - keep the strict `<` comparison

This avoids same-second collisions while keeping NumericDate semantics (seconds since epoch).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/routes/agent_registry.py
@hognek
hognek force-pushed the tsk-nslyhc-registry-token-iat branch 2 times, most recently from fae0522 to ee7a0a5 Compare July 30, 2026 08:46
@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Heads up: this went conflicting today because dev just gained changes in agent_registry_store.py (a new get_by_slug with strict input gating, merged via #2225). When you rebase for the invite-shape criticals the bots flagged, take dev's version of that region and re-apply your token_min_iat changes on top. No rush from my side, flagging so the conflict source is not a mystery.

@hognek
hognek force-pushed the tsk-nslyhc-registry-token-iat branch from ee7a0a5 to e614763 Compare August 2, 2026 17:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
tests/test_token_rotation.py (2)

22-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Reuse the shared client fixture instead of copying it.

The agent_app fixture duplicates about 200 lines of store init and teardown from the main client fixture. Any new store added to app.state must then be updated in two places, and the copy will silently drift. Build agent_app on top of the existing client fixture and only add the agent_registry / agent_grants init that is specific to this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 22 - 236, Refactor the agent_app
fixture to depend on and reuse the existing shared client fixture instead of
duplicating store initialization and teardown. Preserve only the agent_registry
and agent_grants initialization specific to this fixture, and yield the shared
client after that setup without adding parallel cleanup logic.

254-273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify that user_id does not set the record owner.

_register_and_mint passes user_id only to mint_registry_token. registry.register(...) receives no user_id, so record["user_id"] in the route is unrelated to this argument. Any test that wants to exercise the owner branch of require_owner_or_admin cannot do it through this helper. Add an explicit owner argument that is forwarded to register, or rename the parameter to token_user_id.

Also, the three tests in TestTokenMinIatAuth repeat the register/set_status/add_grant sequence instead of calling this helper. Route the tests through the helper to remove the duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 254 - 273, Clarify the ownership
semantics of _register_and_mint by renaming its user_id parameter to
token_user_id, or add a separate owner argument and forward it to
registry.register; ensure owner-branch tests can explicitly set the registered
record owner. Update the three TestTokenMinIatAuth tests to use
_register_and_mint instead of duplicating registration, activation, and grant
setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_token_rotation.py`:
- Around line 384-411: The TestRotateTokensRoute authorization tests currently
reuse the admin session and do not exercise owner or unauthorized access. Update
test_owner_can_rotate to create a distinct non-admin user/session and rotate an
identity owned by that user, then add a test using a different non-admin caller
that asserts a 403 response for the same endpoint; retain the existing
admin-success and nonexistent-identity coverage.

In `@tinyagentos/agent_registry_store.py`:
- Line 440: Update the registry migration chain around
_migration_v6_add_token_min_iat by implementing the missing _migration_v5_*
migration and invoking it in the correct version order. Extend the registry
lookup contract beyond get_by_handle by adding get_by_slug and updating related
registry interfaces or implementations consistently with existing patterns.
- Around line 769-786: Update the route’s audit entry to set after_token_min_iat
from the updated record’s token_min_iat returned by bump_token_min_iat, rather
than the requested ts, so the persisted cutoff is recorded. Preserve the
existing handling for a missing record; only add serialized before/after capture
if the implementation requires exact concurrent rotation pairs.

In `@tinyagentos/routes/agent_registry.py`:
- Around line 771-786: Update the token-rotation flow around bump_token_min_iat
and _audit_governance to use the persisted token_min_iat from updated for
after_token_min_iat, and ensure the response body reports that same value
instead of the requested ts.

---

Nitpick comments:
In `@tests/test_token_rotation.py`:
- Around line 22-236: Refactor the agent_app fixture to depend on and reuse the
existing shared client fixture instead of duplicating store initialization and
teardown. Preserve only the agent_registry and agent_grants initialization
specific to this fixture, and yield the shared client after that setup without
adding parallel cleanup logic.
- Around line 254-273: Clarify the ownership semantics of _register_and_mint by
renaming its user_id parameter to token_user_id, or add a separate owner
argument and forward it to registry.register; ensure owner-branch tests can
explicitly set the registered record owner. Update the three TestTokenMinIatAuth
tests to use _register_and_mint instead of duplicating registration, activation,
and grant setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fb701942-d558-4dee-adb1-a29b484345ba

📥 Commits

Reviewing files that changed from the base of the PR and between 9955383 and e614763.

📒 Files selected for processing (5)
  • tests/test_agent_registry_store.py
  • tests/test_token_rotation.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/routes/agent_registry.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tinyagentos/agent_token_auth.py
  • tests/test_agent_registry_store.py

Comment on lines +384 to +411
class TestRotateTokensRoute:
"""Test POST /api/agents/registry/{id}/rotate-tokens via the admin client."""

@pytest.mark.asyncio
async def test_admin_can_rotate(self, agent_app, app):
"""An admin can bump token_min_iat on any identity."""
cid, _token = await _register_and_mint(app, user_id="admin")
resp = await agent_app.post(f"/api/agents/registry/{cid}/rotate-tokens")
assert resp.status_code == 200
body = resp.json()
assert body["token_min_iat"] > 0

@pytest.mark.asyncio
async def test_owner_can_rotate(self, agent_app, app):
"""A session owner (non-admin) can rotate their own identity."""
cid, _token = await _register_and_mint(app, user_id="admin")
resp = await agent_app.post(f"/api/agents/registry/{cid}/rotate-tokens")
assert resp.status_code == 200
body = resp.json()
assert body["token_min_iat"] > 0

@pytest.mark.asyncio
async def test_rotate_nonexistent_returns_404(self, agent_app):
"""Rotating a nonexistent identity returns 404."""
resp = await agent_app.post(
"/api/agents/registry/no-such-agent-20260101-000000/rotate-tokens"
)
assert resp.status_code == 404

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

test_owner_can_rotate duplicates the admin test and leaves authorization untested.

test_owner_can_rotate uses the same agent_app client, which holds an admin session. Its body is identical to test_admin_can_rotate. The docstring claims a non-admin owner, but no non-admin session is created, so require_owner_or_admin is always satisfied through the admin branch. Two gaps remain:

  1. The owner branch (user.user_id == record["user_id"]) is never executed.
  2. No test asserts 403 for a caller who is neither owner nor admin.

The linked issue lists authorization restrictions as a required verification. Create a second non-admin user, register an identity owned by that user, and add both an owner-success case and a non-owner 403 case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 384 - 411, The
TestRotateTokensRoute authorization tests currently reuse the admin session and
do not exercise owner or unauthorized access. Update test_owner_can_rotate to
create a distinct non-admin user/session and rotate an identity owned by that
user, then add a test using a different non-admin caller that asserts a 403
response for the same endpoint; retain the existing admin-success and
nonexistent-identity coverage.

# Dedupe BEFORE the index so a pre-invariant DB with duplicate active
# handles cannot make the CREATE UNIQUE INDEX (hence boot) fail.
await _migration_v4_dedupe_active_handles(self._db)
await _migration_v6_add_token_min_iat(self._db)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

store="tinyagentos/agent_registry_store.py"

echo "Migration definitions and calls:"
rg -n -P '^\s*(async\s+)?def _migration_v[0-9]+\b|^\s*await _migration_v[0-9]+\(' "$store" || true

echo "Registry lookup implementations and callers:"
rg -n -P '\bget_by_slug\s*\(|\bget_by_handle\s*\(' . --glob '*.py' || true

Repository: jaylfc/taOS

Length of output: 1329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

store="tinyagentos/agent_registry_store.py"
echo "Candidate migration/lookup references:"
rg -n '_migration_v[0-9]+|get_by_slug|get_by_handle|version|schema_version|CREATE TABLE|ALTER TABLE|CREATE INDEX|DROP TABLE' "$store" || true

echo
echo "Store class outline:"
ast-grep outline "$store" --match AgentRegistryStore --view expanded || true

echo
echo "Relevant source ranges:"
sed -n '400,455p' "$store"
printf '\n---\n'
sed -n '388,585p' "$store"
printf '\n---\n'
sed -n '585,695p' "$store"

Repository: jaylfc/taOS

Length of output: 18305


Add the missing registry migration and lookup contract.

tinyagentos/agent_registry_store.py is missing _migration_v5_*, calls _migration_v6_add_token_min_iat without any migration in between, and still only exposes get_by_handle. Add the required v5 migration implementation and any missing get_by_slug or related registry contract updates before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/agent_registry_store.py` at line 440, Update the registry
migration chain around _migration_v6_add_token_min_iat by implementing the
missing _migration_v5_* migration and invoking it in the correct version order.
Extend the registry lookup contract beyond get_by_handle by adding get_by_slug
and updating related registry interfaces or implementations consistently with
existing patterns.

Comment on lines +769 to +786
async def bump_token_min_iat(self, canonical_id: str, ts: int) -> Optional[dict]:
"""Set *canonical_id*'s ``token_min_iat`` to *ts*, invalidating every
token minted before that Unix timestamp.

The caller is responsible for authorisation (admin/session-owner checks).
Returns the updated record, or ``None`` if *canonical_id* does not exist.
"""
if self._db is None:
raise RuntimeError("AgentRegistryStore not initialised")
record = await self.get(canonical_id)
if record is None:
return None
await self._db.execute(
"UPDATE agent_registry SET token_min_iat = MAX(token_min_iat, ?) WHERE canonical_id = ?",
(ts, canonical_id),
)
await self._db.commit()
return await self.get(canonical_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Record the persisted cutoff in the audit entry.

Line [782] uses MAX(token_min_iat, ?), so the stored cutoff can be greater than ts. The route currently records after_token_min_iat=ts, which can make the forensic audit entry report a value that was not applied. Use updated["token_min_iat"]. If concurrent rotations must produce exact before/after pairs, capture both values inside one serialized store operation.

Minimum route fix
-        after_token_min_iat=ts,
+        after_token_min_iat=updated["token_min_iat"],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/agent_registry_store.py` around lines 769 - 786, Update the
route’s audit entry to set after_token_min_iat from the updated record’s
token_min_iat returned by bump_token_min_iat, rather than the requested ts, so
the persisted cutoff is recorded. Preserve the existing handling for a missing
record; only add serialized before/after capture if the implementation requires
exact concurrent rotation pairs.

Comment on lines +771 to +786
ts = int(time.time())
before_iat = record.get("token_min_iat") or 0
updated = await store.bump_token_min_iat(canonical_id, ts)
if updated is None:
return JSONResponse({"error": "not found"}, status_code=404)

await _audit_governance(
request,
action="rotate-tokens",
canonical_id=canonical_id,
actor_user_id=user.user_id,
before_status=record.get("status") or "active",
after_status=updated.get("status") or "active",
before_token_min_iat=before_iat,
after_token_min_iat=ts,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Record the persisted cutoff, not the requested one.

bump_token_min_iat writes MAX(token_min_iat, ?). If the stored token_min_iat is already greater than ts (a previous bump under clock skew, or a manually seeded future cutoff), the database keeps the larger value. The audit event then reports after_token_min_iat=ts, which does not match the persisted row, and the response body reports a different value than the audit entry. Read the value back from updated.

🛠️ Proposed fix
     updated = await store.bump_token_min_iat(canonical_id, ts)
     if updated is None:
         return JSONResponse({"error": "not found"}, status_code=404)
 
+    after_iat = updated.get("token_min_iat") or ts
     await _audit_governance(
         request,
         action="rotate-tokens",
         canonical_id=canonical_id,
         actor_user_id=user.user_id,
         before_status=record.get("status") or "active",
         after_status=updated.get("status") or "active",
         before_token_min_iat=before_iat,
-        after_token_min_iat=ts,
+        after_token_min_iat=after_iat,
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ts = int(time.time())
before_iat = record.get("token_min_iat") or 0
updated = await store.bump_token_min_iat(canonical_id, ts)
if updated is None:
return JSONResponse({"error": "not found"}, status_code=404)
await _audit_governance(
request,
action="rotate-tokens",
canonical_id=canonical_id,
actor_user_id=user.user_id,
before_status=record.get("status") or "active",
after_status=updated.get("status") or "active",
before_token_min_iat=before_iat,
after_token_min_iat=ts,
)
ts = int(time.time())
before_iat = record.get("token_min_iat") or 0
updated = await store.bump_token_min_iat(canonical_id, ts)
if updated is None:
return JSONResponse({"error": "not found"}, status_code=404)
after_iat = updated.get("token_min_iat") or ts
await _audit_governance(
request,
action="rotate-tokens",
canonical_id=canonical_id,
actor_user_id=user.user_id,
before_status=record.get("status") or "active",
after_status=updated.get("status") or "active",
before_token_min_iat=before_iat,
after_token_min_iat=after_iat,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/agent_registry.py` around lines 771 - 786, Update the
token-rotation flow around bump_token_min_iat and _audit_governance to use the
persisted token_min_iat from updated for after_token_min_iat, and ensure the
response body reports that same value instead of the requested ts.

hognek added 2 commits August 2, 2026 19:11
…#2205)

Add token_min_iat INTEGER column (default 0) to agent_registry so tokens
can be invalidated per-identity without revoking the whole identity.

- migration _migration_v6_add_token_min_iat: idempotent ALTER TABLE with
  default 0 so the migration cannot lock the fleet out
- AgentRegistryStore.bump_token_min_iat(canonical_id, ts): persist cutoff
- Auth path (_verify_agent_scope): after the status check, reject tokens
  whose iat claim is < the record's token_min_iat with 401 'token superseded'
- POST /api/agents/registry/{id}/rotate-tokens: session-owner/admin route
  that bumps the cutoff and writes a forensic audit-log entry
- Tests: store-level (bump sets cutoff, default zero, nonexistent nil),
  auth-path (old token rejected, new token passes, default-zero safe),
  route (admin rotates, owner rotates, nonexistent 404)
- bump_token_min_iat: use MAX(token_min_iat, ?) so the cutoff is
  monotonically non-decreasing — a stale/lower timestamp cannot
  silently un-supersede tokens
- rotate-tokens route: handle None from bump_token_min_iat (race
  condition) with 404 instead of AttributeError on updated.get()
- audit entry: capture before_token_min_iat and after_token_min_iat
  in the governance audit payload so rotation values are traceable
- tests: remove unused token bindings in route-level tests
@hognek
hognek force-pushed the tsk-nslyhc-registry-token-iat branch from e614763 to e6fa1e6 Compare August 2, 2026 17:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
tinyagentos/routes/agent_registry.py (1)

279-281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Chain the exception.

Ruff reports B904. Add from exc so the original ValueError stays in the traceback.

♻️ Proposed fix
     except ValueError as exc:
         # The reserved-prefix guard rejected the name. Client error, not 500.
-        raise HTTPException(status_code=422, detail=str(exc))
+        raise HTTPException(status_code=422, detail=str(exc)) from exc

As per static analysis: "Within an except clause, raise exceptions with raise ... from err or raise ... from None".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/agent_registry.py` around lines 279 - 281, Update the
HTTPException raise in the ValueError handler to explicitly chain it from the
caught exc, preserving the original ValueError traceback while keeping the
existing 422 status and detail.

Source: Linters/SAST tools

tests/test_token_rotation.py (2)

304-306: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The assertion does not check what the comment states.

The comment says token_min_iat is 0, but the assertion only checks that the record exists. Assert the field directly.

💚 Proposed fix
-        assert (await registry.get(cid)) is not None  # token_min_iat is 0
+        assert (await registry.get(cid))["token_min_iat"] == 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 304 - 306, Update the assertion
after minting old_token to inspect the registry record’s token_min_iat field
directly and verify it equals 0, rather than only asserting that
registry.get(cid) returns a record.

22-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse client, then add only agent_registry / agent_grants init.

agent_app duplicates the root client fixture from tests/conftest.py, including admin setup and teardown. Inject client and add the registry-specific init before yielding it, then remove the copied store init/close block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 22 - 236, Refactor the agent_app
fixture to accept the existing client fixture, initialize only
app.state.agent_registry and app.state.agent_grants when needed, and yield
client directly. Remove the duplicated admin-session setup, store
initialization, and teardown logic from agent_app while preserving the
registry-specific initialization before yielding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_token_rotation.py`:
- Around line 254-273: Update the _register_and_mint helper so its
registry.register call passes the received user_id value, ensuring the persisted
agent record owner matches the user embedded in the minted token while
preserving the existing default behavior.

---

Nitpick comments:
In `@tests/test_token_rotation.py`:
- Around line 304-306: Update the assertion after minting old_token to inspect
the registry record’s token_min_iat field directly and verify it equals 0,
rather than only asserting that registry.get(cid) returns a record.
- Around line 22-236: Refactor the agent_app fixture to accept the existing
client fixture, initialize only app.state.agent_registry and
app.state.agent_grants when needed, and yield client directly. Remove the
duplicated admin-session setup, store initialization, and teardown logic from
agent_app while preserving the registry-specific initialization before yielding.

In `@tinyagentos/routes/agent_registry.py`:
- Around line 279-281: Update the HTTPException raise in the ValueError handler
to explicitly chain it from the caught exc, preserving the original ValueError
traceback while keeping the existing 422 status and detail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3319af71-b23f-46aa-84cc-bc4bb028b3d0

📥 Commits

Reviewing files that changed from the base of the PR and between e614763 and e6fa1e6.

📒 Files selected for processing (5)
  • tests/test_agent_registry_store.py
  • tests/test_token_rotation.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/routes/agent_registry.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tinyagentos/agent_token_auth.py
  • tests/test_agent_registry_store.py
  • tinyagentos/agent_registry_store.py

Comment on lines +254 to +273
async def _register_and_mint(app, *, user_id="u", scopes=("a2a_receive",)):
"""Register an active agent, add grants, and mint a signed JWT.

Returns (canonical_id, token).
"""
registry = app.state.agent_registry
grants = app.state.agent_grants
priv, _pub = app.state.agent_registry_keypair
rec = await registry.register(
framework="test",
display_name="TestAgent",
origin="external-selfjoin",
handle="@test",
)
cid = rec["canonical_id"]
await registry.set_status(cid, "active")
for scope in scopes:
await grants.add_grant(cid, scope)
token = mint_registry_token(cid, priv, user_id=user_id, framework="test")
return cid, token

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

user_id is not persisted on the registry record.

_register_and_mint accepts user_id but passes it only to mint_registry_token. The call to registry.register(...) omits user_id=, so the stored row keeps the default owner value. Any test that relies on record ownership (for example the owner branch of require_owner_or_admin) cannot work with this helper.

🐛 Proposed fix
     rec = await registry.register(
         framework="test",
         display_name="TestAgent",
+        user_id=user_id,
         origin="external-selfjoin",
         handle="`@test`",
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def _register_and_mint(app, *, user_id="u", scopes=("a2a_receive",)):
"""Register an active agent, add grants, and mint a signed JWT.
Returns (canonical_id, token).
"""
registry = app.state.agent_registry
grants = app.state.agent_grants
priv, _pub = app.state.agent_registry_keypair
rec = await registry.register(
framework="test",
display_name="TestAgent",
origin="external-selfjoin",
handle="@test",
)
cid = rec["canonical_id"]
await registry.set_status(cid, "active")
for scope in scopes:
await grants.add_grant(cid, scope)
token = mint_registry_token(cid, priv, user_id=user_id, framework="test")
return cid, token
async def _register_and_mint(app, *, user_id="u", scopes=("a2a_receive",)):
"""Register an active agent, add grants, and mint a signed JWT.
Returns (canonical_id, token).
"""
registry = app.state.agent_registry
grants = app.state.agent_grants
priv, _pub = app.state.agent_registry_keypair
rec = await registry.register(
framework="test",
display_name="TestAgent",
user_id=user_id,
origin="external-selfjoin",
handle="`@test`",
)
cid = rec["canonical_id"]
await registry.set_status(cid, "active")
for scope in scopes:
await grants.add_grant(cid, scope)
token = mint_registry_token(cid, priv, user_id=user_id, framework="test")
return cid, token
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_token_rotation.py` around lines 254 - 273, Update the
_register_and_mint helper so its registry.register call passes the received
user_id value, ensuring the persisted agent record owner matches the user
embedded in the minted token while preserving the existing default behavior.

hognek added 2 commits August 2, 2026 20:14
Docs-Reviewed: retrigger CI after author identity fix; no API surface changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants